---
title: "Learner Bot — API Endpoints"
type: concept
created: 2026-04-18
updated: 2026-04-18
sources: ["raw/articles/02-api-contracts.md"]
tags: [learner-bot, api, endpoints, internal]
---

# Bot Endpoints

All [[Learner Bot]] endpoints require `X-Internal-Key: <INTERNAL_KEY> (stored in secure env — see .env)` header. No uc_session cookie accepted.

Base URL: `http://learner-bot.readingtester.com` (internal only, port 3120)

## GET /api/v1/bot/:learner_id/status

Full learner status — primary read endpoint for [[Teacher Portal]].

**Output:**
```json
{
  "reading_level": 3.5,
  "books_read": 12,
  "total_miles": 4.2,
  "readingLevelHistory": [{"level": 3.0, "date": "2026-04-01"}],
  "booksCompleted": ["14760-1"],
  "comprehensionTrend": [{"date": "2026-04-18", "score": 78}],
  "latestReport": "Sofia has been working on past tense...",
  "vocab_gaps": [{"word": "magnificent", "tap_count": 3}],
  "curriculum_state": {"objective_id": "Y3-L4", "value": "assessment", "mastery": 0.67}
}
```

**Errors:** 404 if learner not found

## POST /api/v1/bot/:learner_id/quiz-result

Store assessment result and trigger immediate bot run.

**Input:**
```json
{
  "bookId": "14760-1",
  "fkLevel": 3.5,
  "scorePct": 78.5,
  "questionsTotal": 5,
  "questionsCorrect": 4,
  "levelRecommendation": "hold"
}
```

`levelRecommendation` enum: `drop | hold | raise`
- `drop` = score < 65% — book too hard
- `hold` = 65–85% — good match
- `raise` = score > 85% — child can advance

**Output:** `{ "ok": true }`

**Side effect:** triggers `setImmediate(() => runBotForLearner(learnerId))` — bot processes asynchronously

## POST /api/v1/telemetry/vocab-taps

Upserts vocabulary gap counts. Called by [[Telemetry Service]] on session end.

**Input:**
```json
{
  "learner_id": "uuid",
  "session_id": "uuid",
  "words": [
    {"word": "magnificent", "count": 2},
    {"word": "photosynthesis", "count": 1}
  ]
}
```

**Output:** `{ "ok": true, "upserted": 2 }`

**Logic:** UPSERT into `vocabulary_gaps` — `tap_count += count`, `last_tapped = NOW()`

## POST /api/v1/telemetry/session-summary

Stores reading pattern and engagement signals. Called by [[Telemetry Service]] on session end.

**Input:**
```json
{
  "learner_id": "uuid",
  "session_id": "uuid",
  "pages_read": 12,
  "total_pages": 15,
  "avg_time_per_page_ms": 48600,
  "slow_pages": [3, 5, 7]
}
```

**Logic:**
- confidence = 0.90 if `pages_read/total_pages > 0.8` else 0.60
- If `slow_pages.length > 3` → store `engagement_signal {signal: "struggling"}`

**Output:** `{ "ok": true }`

## GET /api/v1/bot/:learner_id/digest

Warm-tone parent digest. Called by [[Parent Portal]].

**Output:** `{ "digest": "string (warm-tone summary for parent)" }`

## POST /api/v1/bot/:learner_id/placement-result

Store initial placement test result. Called after first login placement test.

**Input:**
```json
{
  "learner_id": "uuid",
  "fk_level": 3.5,
  "raw_scores": {}
}
```

**Output:** `{ "ok": true }`

**Logic:** Stores `reading_level` memory with confidence = 1.0 (authoritative initial value)

## POST /api/v1/bot/:learner_id/fluency-result

Store fluency assessment result. Called by Fluency Assessment service.

**Status:** UNVERIFIED — not yet wired. Fluency scores currently siloed.

**Input:**
```json
{
  "fk_level": 3.2,
  "cefr_level": "B1",
  "wcpm": 85
}
```

## Telemetry → Bot Delivery Contract

Telemetry → Bot calls (vocab-taps, session-summary, quiz-result):

| Property | Behavior |
|---|---|
| Attempts | 3 (with exponential backoff: 1s, 4s, 16s) |
| Timeout per attempt | 5s |
| On all 3 failures | Write to `telemetry_delivery_failures` table with payload, session_id, error, timestamp |
| Recovery | Nightly reconciliation job re-processes any rows in `telemetry_delivery_failures` older than 5 min |
| Data loss guarantee | Zero: payload is preserved in failure table until successfully delivered |
| Ordering | Best-effort FIFO per session_id; bot is idempotent on duplicate delivery |

**Reliability guarantee:** no telemetry event is silently dropped. If the bot is down, data queues in `telemetry_delivery_failures` and is delivered on recovery.

> **Needs Approval:** Replace delivery with durable Redis/BullMQ queue if bot call volume requires it. Current design is correct and production-safe for v1 single-server topology.
